Skip to content
alex.dev

May 7, 2026

8 min read

Understanding Closures, Finally

#javascript #fundamentals

I nodded along to closure explanations for years before I actually understood them. The definitions were all technically correct and completely unhelpful: "a closure is a function bundled with references to its lexical environment." Sure. Here's the version that finally worked for me.

Functions carry a backpack

When you create a function inside another function, the inner function packs a little backpack. Into that backpack go the variables it uses from the outer scope — not copies of them, the variables themselves. Wherever that inner function travels, the backpack goes with it.

function makeCounter() {
  let count = 0;
  return function () {
    count = count + 1;
    return count;
  };
}

const tick = makeCounter();
tick(); // 1
tick(); // 2

makeCounter finished running long ago. Its local variables should be gone — but count lives on, because tick carries it in its backpack. That's the whole trick. Everything else is detail.

The loop bug everyone hits

The classic closure bug is creating functions in a loop with var:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));
}
// 3, 3, 3

All three callbacks packed the same i into their backpacks — there's only one, and by the time they run it's 3. Declaring it with let gives every loop iteration its own fresh i, so each callback packs a different variable. Same code, let instead of var, and it logs 0, 1, 2.

Closures you already use daily

Once it clicks, you see closures everywhere. Every event handler that references a variable from the surrounding component: closure. Every debounce helper that remembers its timer: closure. Every module that exposes functions over private state: closure. You don't need permission to use them — you already do.

One habit worth keeping

The only real footgun is holding a backpack you didn't mean to keep. A long-lived closure keeps everything it references alive, which is how you accidentally pin a huge parsed payload in memory because one small callback still points at it. When a closure outlives the moment that created it, glance at what it's carrying — and let go of what it doesn't need.